package org.interaction3d.assembly.sax;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Locale;
import java.util.Scanner;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.interaction3d.assembly.CoordinateArray;
import static javax.xml.XMLConstants.*;
import org.interaction3d.assembly.Mesh;
import org.interaction3d.assembly.PrimitiveType;
import org.interaction3d.assembly.Primitives;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;



public class SAXMeshReader
{
    // <editor-fold defaultstate="collapsed" desc=" MeshHandler ">

    private static class MeshHandler extends DefaultHandler
    {
        private final Mesh mesh;

        private int vertices;
        // coordinates
        private String name;
        private int dimension, split, count;
        //mapping
        private int[][] idMap;
        private int index, numMaps, selected;
        private CoordinateArray coordinateArray;


        private Locator locator;
        private AttributeParser ap;
        private boolean readCData = false;
        private StringBuilder cdata = new StringBuilder();

        public MeshHandler(Mesh mesh)
        {
            if(mesh == null)
            {
                throw new NullPointerException();
            }

            this.mesh = mesh;
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException
        {
            if (readCData)
                this.cdata.append(ch, start, length);
        }

        @Override
        public void setDocumentLocator(Locator locator)
        {
            this.ap = new AttributeParser(locator);
            this.locator = locator;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
        {
            if (qName.equals("Mesh"))
            {

            }
            else if (qName.equals("Vertices"))
            {
                this.vertices = this.ap.parseInt(attributes, "count");
            }
            else if (qName.equals("Elements"))
            {
                final int numVertexElems = this.ap.parseInt(attributes, "count");
                idMap = new int[numVertexElems][2];
                index = 0; selected = 0;
                mesh.coordinateArrays(numVertexElems);
            }
            else if (qName.equals("Element"))
            {
                this.name = this.ap.parseString(attributes, "name");
                idMap[index][1] = this.ap.parseInt(attributes, "map", -1);
            }
            else if (qName.equals("Coordinates"))
            {
                this.dimension = this.ap.parseInt(attributes, "dimension");
                this.count = this.ap.parseInt(attributes, "count");
                this.split = this.ap.parseInt(attributes, "split", count);


                coordinateArray = mesh.coordinateArray(name, dimension, count, split);
                boolean use = coordinateArray != null;

                idMap[index++][0] = use
                                  ? selected++
                                  : -1;

                this.cdata.setLength(0);
                this.readCData = true;
            }
            else if (qName.equals("Split"))
            {
                this.readCData = false;
            }
            else if (qName.equals("Mapping"))
            {
                mesh.vertices(this.vertices);

                this.numMaps = this.ap.parseInt(attributes, "count");

                cdata.setLength(0);
                readCData = true;
            }
            else if (qName.equals("Primitives"))
            {
                final int numGroups = this.ap.parseInt(attributes, "groups");
                mesh.primtives(new PrimitiveCounts(numGroups, attributes, ap));
            }
            else if (qName.equals("Triangles"))
            {
                count = this.ap.parseInt(attributes, "count");
                String triGroupName = attributes.getValue("material");

                mesh.primtiveGroup(triGroupName, PrimitiveType.TRIANGLE, count);

                cdata.setLength(0);
                readCData = true;
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException
        {
            this.readCData = false;

            if (qName.equals("Split"))
            {
                parseCoordinates(cdata, dimension, split, coordinateArray);
                cdata.setLength(0);
                this.readCData = true;
            }
            else if (qName.equals("Coordinates"))
            {
                if (count > this.split)
                    parseCoordinateRefs(this.cdata, count-split, coordinateArray);
                else
                    parseCoordinates(cdata, dimension, split, coordinateArray);
            }
            else if (qName.equals("Mapping"))
            {
                int[] selection = new int[selected];

                for(int i=0; i<idMap.length; i++)
                {
                    int id = idMap[i][0];
                    if(id >= 0)
                    {
                        int map = idMap[i][1];
                        selection[id] = map;
                    }
                }

                parseVertices(this.cdata, vertices, numMaps, selection, mesh);
            }
            else if (qName.equals("Triangles"))
            {
                parseTriangles(cdata, count, mesh);
            }
        }

        @Override
        public void error(SAXParseException e)
        {
            String msg = "Error at " + locator.getLineNumber()  + ":" + locator.getColumnNumber();
            handle(e, msg);
        }

        @Override
        public void fatalError(SAXParseException e)
        {
            String msg = "Fatal error at " + locator.getLineNumber()  + ":" + locator.getColumnNumber();
            handle(e, msg);
        }

        @Override
        public void warning(SAXParseException e)
        {
            String msg = "Warning at " + locator.getLineNumber()  + ":" + locator.getColumnNumber();
            handle(e, msg);
        }
    }

    static private void parseVertices(CharSequence coordinates, int count, int elemets, int[] selection, Mesh mesh)
    {
        final Scanner scanner = new Scanner(coordinates.toString()).useLocale(Locale.ENGLISH);

        int[] allElems = new int[elemets];
        int[] selectedElems = new int[selection.length];

        for(int numMatches=0; numMatches<count; numMatches++)
        {
            for(int i=0; i<allElems.length; i++)
            {
                allElems[i] = scanner.nextInt();
            }

            for(int i=0; i<selection.length; i++)
            {
                selectedElems[i] = (selection[i] < 0)
                                 ? numMatches
                                 : allElems[selection[i]];
            }

            mesh.vertex(selectedElems);
        }
    }


    static private void parseCoordinates(CharSequence coordinates, int dimension, int count, CoordinateArray mesh)
    {
        final Scanner scanner = new Scanner(coordinates.toString()).useLocale(Locale.ENGLISH);

        
        double[] coord = new double[dimension];
        for(int numMatches=0; numMatches<count; numMatches++)
        {
            for(int i=0; i<coord.length; i++)
            {
                coord[i] = scanner.nextDouble();
            }
            mesh.coordinate(coord);
        }
    }

    static private void parseCoordinateRefs(CharSequence coordinates, int count, CoordinateArray mesh)
    {
        final Scanner scanner = new Scanner(coordinates.toString()).useLocale(Locale.ENGLISH);
        
        for(int numMatches=0; numMatches<count; numMatches++)
        {            
            mesh.coordinate(scanner.nextInt());
        }
    }

    static private void parseTriangles(CharSequence coordinates, int count, Mesh mesh)
    {
        final Scanner scanner = new Scanner(coordinates.toString()).useLocale(Locale.ENGLISH);

        int[] triangle = new int[3];
        for(int numMatches=0; numMatches<count; numMatches++)
        {
            for(int i=0; i<triangle.length; i++)
            {
                triangle[i] = scanner.nextInt();
            }
            mesh.primtive(triangle);
        }
    }

    static private final class PrimitiveCounts implements Primitives
    {
        private final int trinagles;
        private final int trinagleGroups;

        public PrimitiveCounts(int groups, Attributes attributes, AttributeParser ap) throws SAXParseException
        {
            trinagles = ap.parseInt(attributes, "triangles", 0);
            trinagleGroups = groups;
        }

        public int count(PrimitiveType type)
        {
            switch(type)
            {
                case TRIANGLE:
                    return trinagles;
                default:
                        return 0;
            }
        }

        public int groups(PrimitiveType type)
        {
            switch(type)
            {
                case TRIANGLE:
                    return trinagleGroups;
                default:
                        return 0;
            }
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Variables ">

    private final SAXParser parser;

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Constructors ">

    private SAXMeshReader(SAXParserFactory factory) throws ParserConfigurationException, SAXException
    {        
        URL schemaURL = getClass().getResource("/org/interaction3d/assembly/Mesh.xsd");
        Schema schema = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI).newSchema(schemaURL);
        factory.setSchema(schema);        
        factory.setNamespaceAware(true);//impoartant for xsd
        factory.setValidating(false);//only for dtd
        parser = factory.newSAXParser();
    }

    public SAXMeshReader() throws ParserConfigurationException, SAXException, IllegalArgumentException, IOException
    {
        this(SAXParserFactory.newInstance());
    }
    
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Methods ">

    public Mesh.Assembly assembly(final String uri)
    {
        return new Mesh.Assembly()
        {
            public void assemble(Mesh mesh)
            {
                try
                {
                    parser.parse(uri, new MeshHandler(mesh));
                }
                catch (SAXException ex)
                {
                    handle(ex);
                }
                catch (IOException ex)
                {
                    handle(ex);
                }
            }
        };
    }

    public Mesh.Assembly assembly(final InputSource is)
    {
        return new Mesh.Assembly()
        {
            public void assemble(Mesh mesh)
            {
                try
                {
                    parser.parse(is, new MeshHandler(mesh));
                }
                catch (SAXException ex)
                {
                    handle(ex);
                }
                catch (IOException ex)
                {
                    handle(ex);
                }
            }
        };
    }

    public Mesh.Assembly assembly(final InputStream in)
    {
        return new Mesh.Assembly()
        {
            public void assemble(Mesh mesh)
            {
                try
                {
                    parser.parse(in, new MeshHandler(mesh));
                }
                catch (SAXException ex)
                {
                    handle(ex);
                }
                catch (IOException ex)
                {
                    handle(ex);
                }
            }
        };
    }

    public Mesh.Assembly assembly(final File f)
    {
        return new Mesh.Assembly()
        {
            public void assemble(Mesh mesh)
            {
                try
                {
                    parser.parse(f, new MeshHandler(mesh));
                }
                catch (SAXException ex)
                {
                    handle(ex);
                }
                catch (IOException ex)
                {
                    handle(ex);
                }
            }
        };
    }

    // </editor-fold>

    static private void handle(Exception ex)
    {
        handle(ex, null);
    }

    static private void handle(Exception ex, String msg)
    {
        Logger.getLogger(SAXMeshReader.class.getName()).log(Level.SEVERE, msg, ex);
        throw new RuntimeException(ex);
    }

    static
    {
        Logger logger = Logger.getLogger(SAXMeshReader.class.getName());
        logger.setLevel(Level.ALL);
        logger.addHandler(new ConsoleHandler());        
    }
}

